今天我們針對schema,分享一些進階的概念。
Abstract constraint可以幫助我們自己定義想要的constraint。例如:
abstract constraint must_contain_a() {
    errmessage :=
    '{__subject__} must contain at least one `a` or `A`.';
    using ( 
        contains(str_lower(__subject__), "a") 
    ) ;
}
type User {
    name: str {
        constraint must_contain_a
    }
}
這裡我們定義一個abstract constraint must_contain_a來確認其自身必須包含最少一個「"a"」或「"A"」字母。接著我們將must_contain_a施加於User object的name property。比較特別的是可以在errmessage中使用{__subject__}來顯示自身,有點像是Python的f-string功能。
此時,我們試著insert一個name property為「"John"」的User object:
select(insert User {name:="John"}) {name};
因為John內並沒有「"a"」或「"A"」字母,所以會報錯如下:
edgedb error: ConstraintViolationError: name must contain at least one `a` or `A`.
Detail: violated constraint 'default::must_contain_a' on property 'name' of object type 'default::User'
如果insert name property為「"May"」或「"MAY"」的User object,則皆會成功:
with names:= {"May", "MAY"}
for name in names
union (
  select (insert User{name:=name}) {name}
);
{default::User {name: 'May'}, default::User {name: 'MAY'}}
abstract link可以幫助我們建立抽象化的link,使其可以作用在多個object type。
考慮schema如下:
abstract link link_with_note {
    note: str;
}
type Person {
    name: str;
    multi friends: Person {
        extending link_with_note;
    };
}
可以看出abstract link link_with_note可以經由Person object extending後,作為Person object中的multi friends link;而abstract link link_with_note中的note則可以經由Person object以link property型式來存取。
此時我們insert兩個name property為「"John"」及「"Tom"」的Person object,並以John及Tom來代稱:
with names:= {"John", "Tom"}
for name in names
union (
    select(insert Person {name:=name}) {name}
);
{default::Person {name: 'John'}, default::Person {name: 'Tom'}}
假設John是個常常記不清楚,朋友是在哪個階段認識的。此時他可以更新自己的multi friends link中的note link property來註記Tom是自己的高中同學:
with john:= (select Person filter .name="John"),
     tom:= (select Person filter .name="Tom")
update john
set {
    friends:= tom {@note:= "high school classmate"}
};
可以使用下面query確認註記成功:
select Person {**};
{
  default::Person {
    id: 7cc79298-53f1-11ef-926f-7364dedf390e,
    name: 'John',
    friends: {
      default::Person {
        id: 7cc794f0-53f1-11ef-926f-d7166079d714, 
        name: 'Tom', 
        @note: 'high school classmate'},
    },
  },
  default::Person {
    id: 7cc794f0-53f1-11ef-926f-d7166079d714, 
    name: 'Tom', 
    friends: {}},
}
不知道大家有沒有感受到,link property其實是個很有趣的功能呀?我自己是將link property想像為link的metadata,可以用來存取一些額外的資訊,並經由object type來存取。
最後提醒大家,官方文件中提到link property只能是single與optional。
本小節取材自Easy EdgeDB第十七章第四個練習題。如何能夠在保存data的情況下,修改object type的schema,將其中的property抽取出來為獨立的abstract type?
考慮schema如下:
type User {
    email: str;
}
此時我們insert一個User object:
select (insert User {email:="John@example.com"}) {email};
{default::User {email: 'John@example.com'}}
此時我們可以將email property抽取為HasEmail,並改寫User object來extending HasEmail:
abstract type HasEmail {
    email: str;
}
type User extending HasEmail;
接著執行migration:
did you create object type 'default::HasEmail'? [y,n,l,c,b,s,q,?]
> y
did you alter object type 'default::User'? [y,n,l,c,b,s,q,?]
> y
The following extra DDL statements will be applied:
    ALTER TYPE default::User {
        ALTER PROPERTY email {
            DROP OWNED;
            RESET TYPE;
        };
    };
(approved as part of an earlier prompt)
此時可以確認原先的User object的確仍然在資料庫內:
select User {*};
{default::User {id: d41b9f02-5406-11ef-a0e8-bb18dd29e982, email: 'John@example.com'}}
這個抽取技巧有點類似Python的Mixin或是Rust的trait。
本小節取材自官方文件。
backlink與multi link即是EdgeDB中的many-to-one及one-to-many關係。
舉例來說,我們將針對下面這個情況,分別使用backlink與multi link兩種方式來寫寫看:
User object可以擁有多件Shirt object。Shirt object只能被一個User object所擁有。backlink的想法是many-to-one,可以想成是many Shirt object to one Person object。
schema定義如下:
type Person {
    required name: str
}
type Shirt {
    required color: str;
    owner: Person;
}
執行下面query,生成一個Person object及三個Shirt object:
insert Person {name:="John"};
with colors:= {"red", "green", "blue"}
for color in colors
union (
    insert Shirt {
        color:=color, 
        owner:= assert_single(Person)
    }
);
此時,如果我們想由Person object下手,取得其所擁有的Shirt object時,可以寫為:
select Person {name, shirts:= .<owner[is Shirt]{color} };
{
  default::Person {
    name: 'John',
    shirts: {
        default::Shirt {color: 'red'}, 
        default::Shirt {color: 'green'}, 
        default::Shirt {color: 'blue'}
    },
  },
}
可以看出來shirts原本是沒有定義在schema內,而是我們使用backlink所取得的。
如果這樣的運算很常使用的話,也可以將其直接定義於schema內。例如:
type Person {
    required name: str
    shirts:= .<owner[is Shirt]
}
type Shirt {
    required color: str;
    owner: Person;
}
此時依然可以從Person object中取得其所擁有的Shirt object:
select Person {name, shirts: {color}};
{
  default::Person {
    name: 'John',
    shirts: {
        default::Shirt {color: 'red'}, 
        default::Shirt {color: 'green'}, 
        default::Shirt {color: 'blue'}
    },
  },
}
multi link的想法是one-to-many,可以想成是one Person object to many Shirt object。
schema定義如下:
type Person {
    required name: str;
    multi shirts: Shirt {
    # ensures a one-to-many relationship
    constraint exclusive;
    }
}
type Shirt {
    required color: str;
}
其中的constraint exclusive非常重要,可以確保一個Shirt object只能被一個Person object擁有。
執行下面query,生成一個Person object及三個Shirt object:
with colors:= {"red", "green", "blue"}
for color in colors
union (
    insert Shirt {
        color:=color, 
    }
);
insert Person {name:="John", shirts:= Shirt};
由Person object中取得其所擁有的Shirt object:
select Person {name, shirts: {color}};
{
  default::Person {
    name: 'John',
    shirts: {
        default::Shirt {color: 'red'}, 
        default::Shirt {color: 'green'}, 
        default::Shirt {color: 'blue'}
    },
  },
}
官方文件中建議當有下列兩種情況的時候使用multi link,否則建議使用single link搭配backlink:
annotation可以幫助我們提供一些註記給object type。EdgeDB預設有title、description及deprecated三種。
考慮schema如下:
type User {
    annotation deprecated := "BREAKING CHANGE! As of version x.y.z, the `User` object will be renamed to `USER`."
}
我們給予User object一個deprecated annotation。
此時可以利用introspect可以檢視User object的內部資訊:
select (introspect User) { annotations: {name, @value}};
{
  schema::ObjectType {
    annotations: {
      schema::Annotation {
        name: 'std::deprecated',
        @value: 'BREAKING CHANGE! As of version x.y.z, the `User` object will be renamed to `USER`.',
      },
    },
  },
}
除了預設的三種annotation外,我們也可以自己定義。例如,這裡我們自己定義了一個hello annotation:
abstract annotation hello;
type User {
    annotation hello := "hello"
}
此時一樣可以利用introspect來檢視User object:
select (introspect User) { annotations: {name, @value}};
{
  schema::ObjectType {
    annotations: {
      schema::Annotation {
        name: 'default::hello', 
        @value: 'hello'
      }
    }
  }
}
可以確認User object確實註記有hello annotation。
Trigger就像一個callback,可以在object type進行某一種操作後,接著執行另一個操作(但兩者會在同一個transaction內)。
rewrite則是攔截insert或update等mutation指令,並依據預先設定的expression來改寫property或link後,再傳至database。
官方文件中提到這個例子:
type User {
    required name: str;
    trigger log_insert after insert for each do (
        insert Log {
          action := 'insert',
          target_name := __new__.name
        }
    );
}
在每次insert一個User object同時,也會insert一個Log object。
此外,文件中也提到trigger可能會引發callback地獄,需要小心使用。
官方文件中提到這個例子:
type Post {
    required title: str;
    required body: str;
    modified: datetime {
        rewrite insert, update using (datetime_of_statement())
    }
}
在每一次進行insert或update時,EdgeDB會自動攔截並改寫query,將執行datetime_of_statement()後的值指定給modified property,再傳給database。
EdgeDB在下面三個地方,會自動幫大家進行index:
object自動產生的id(可以想成primary key)。link(可以想成foreign key)。constraint exclusive的property。此外,也可以直接使用Postgres提供的index。